/**
 * 
 */
package com.google.code.rookery.converter;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipException;

import org.apache.log4j.Logger;

/**
 * Base class for converters for converting from a type of object to another
 * @author Thom Hehl
 */
public abstract class InstanceConverter {
  public static final String EXT_CLASS      = ".class";
  public static final String EXT_JAVA       = ".java";

  public static final String JAR_PROLOG     = "jar:file:/";

  private static final String CONVERTER_METHOD  = "convert";
  private static final String CONVERTER_PACKAGE = "com.google.code.rookery.converter";

  protected static Map<ConverterKey, InstanceConverter>
                            converterMap = new HashMap<ConverterKey, InstanceConverter>();

  private static Logger     _log = Logger.getLogger(InstanceConverter.class);

  /**
   * Search for a converter from one class to another
   * @param fromClass the class being converted from
   * @param toClass the class being converted to
   * @return the converter, if found, or null otherwise
   * @throws IllegalAccessException 
   * @throws InstantiationException 
   */
  public static InstanceConverter findConverter(Class<?> fromClass, Class<?> toClass)
      throws ClassNotFoundException, InstantiationException, IllegalAccessException{
    ConverterKey key = new ConverterKey(fromClass, toClass);
    InstanceConverter ret = converterMap.get(key);
    //if a converter wasn't found and it doesn't exist as a key
    if(ret==null && !converterMap.containsKey(key)){
      //this pair hasn't been looked up before, do the search
      for(Class<?> clazz:getClassesInPackage(CONVERTER_PACKAGE)){
        for(Method method:clazz.getMethods()){
          //if it's a converter method
          if(CONVERTER_METHOD.equals(method.getName())){
            //if the return type is right
            if(toClass.equals(method.getReturnType())){
              //check the parameters
              Class<?>[] parmTypes = method.getParameterTypes();
              if(parmTypes.length == 1 && fromClass.equals(parmTypes[0])){
                //this is the converter
                ret = (InstanceConverter) clazz.newInstance();
                break;
              }
            }
          }
        }
      }
      //store for future use
      converterMap.put(key, ret);
    }
    return ret;
  }

  /**
   * Add the file to the collection if it's a class, or recurse the file if it's a directory
   * @param pckgName the name of the package
   * @param file the file/directory being checked
   * @param classes the list of classes being added to
   * @throws ClassNotFoundException 
   */
  @SuppressWarnings("rawtypes")
  protected static void findClasses(String pckgName, File file, ArrayList<Class> classes) throws ClassNotFoundException {
    //recurse the directory
    if(file.isDirectory()){
      for(File entry:file.listFiles()){
        String newPckg = pckgName + "." + file.getName();
        findClasses(newPckg, entry, classes);
      }
    }
    else{
      // add the .class or .java files
      String name = file.getName();
      if(name.endsWith(EXT_CLASS)) {
        // removes the .class extension
        String className = pckgName + '.' + name.substring(0, name.length()-EXT_CLASS.length());
        classes.add(Class.forName(className));
      }
      else if(name.endsWith(EXT_JAVA)) {
        // removes the .class extension
        String className = pckgName + '.' + name.substring(0, name.length()-EXT_JAVA.length());
        classes.add(Class.forName(className));
      }
    }
  }

  /**
   * Retrieve all of the classes from the supplied package name
   * @param pckgName the name of the package
   * @return an array of classes
   * @throws ClassNotFoundException
   */
  @SuppressWarnings({"rawtypes"})
  public static Class[] getClassesInPackage(String pckgName) throws ClassNotFoundException {
    ArrayList<Class> classes = new ArrayList<Class>();
    // set up the file listing
    //loop through each match on the package name to find the one with the most entries
    try {
      Enumeration<URL> en = Thread.currentThread().getContextClassLoader().getResources(pckgName.replace('.', '/'));
      while(en.hasMoreElements()){
        URI uri;
        try {
          uri = en.nextElement().toURI();
        } catch (URISyntaxException use) {
          throw new ClassNotFoundException("Error converting url", use);
        }
        File dir = null;
        try {
          dir = new File(uri);
        } catch (IllegalArgumentException iae) {
          //not a traditional file. Must be in a jar.
          String jarName = uri.toString();
          jarName = urlDecode(jarName);
          int pos = jarName.indexOf('!');
          if(pos <0){
            throw new ClassNotFoundException("Unable to parse name of jar file:" + jarName);
          }
          jarName = jarName.substring(0, pos);
          if(jarName.startsWith(JAR_PROLOG)){
            jarName = jarName.substring(JAR_PROLOG.length());
          }
          String pathString = pckgName.replaceAll("\\.", "/");
          JarFile jar;
          try {
            jar = new JarFile(jarName);
          } catch (FileNotFoundException fnfe) {
            //try adding back in the leading slash for UNIX
            jarName = "/" + jarName;
            try {
              jar = new JarFile(jarName);
            } catch (FileNotFoundException fnfe2) {
              throw new ClassNotFoundException("Error opening:" + jarName, fnfe2);
            }
          } catch (ZipException ze) {
            //try adding back in the leading slash for UNIX
            jarName = "/" + jarName;
            try {
              jar = new JarFile(jarName);
            } catch (ZipException ze2) {
              throw new ClassNotFoundException("Error opening:" + jarName, ze2);
            }
          }
          Enumeration<JarEntry> enumEntries = jar.entries();
          while(enumEntries.hasMoreElements()){
            JarEntry entry = enumEntries.nextElement();
            String name = entry.getName();
            if(name.startsWith(pathString) && !name.contains("$")){
              try {
                if(name.endsWith(EXT_CLASS)) {
                  // removes the .class extension
                  String className = name.substring(0, name.length()-EXT_CLASS.length()).replaceAll("/","\\.");
                  classes.add(Class.forName(className));
                }
                else if(name.endsWith(EXT_JAVA)) {
                  // removes the .class extension
                  String className = name.substring(0, name.length()-EXT_JAVA.length()).replaceAll("/","\\.");
                  classes.add(Class.forName(className));
                }
              } catch (NoClassDefFoundError ncdfe) {
                //sometimes, super classes are not found, so just continue
                _log.warn(ncdfe.getMessage(), ncdfe);
              }
            }
            //classes.add(entry.getName());
          }
        }
        if(dir!=null){
          File[] flist = dir.listFiles();
          for(File file:flist){
            findClasses(pckgName, file, classes);
          }
        }
      }
    } catch(FileNotFoundException fnfe) {
      throw new ClassNotFoundException(pckgName + " does not appear to be a valid package", fnfe);
    } catch(NullPointerException npe) {
      throw new ClassNotFoundException(pckgName + " does not appear to be a valid package", npe);
    } catch (IOException ioe) {
      throw new ClassNotFoundException(pckgName + " does not appear to be a valid package", ioe);
    }
    // Get the list of the files contained in the package
    Class[] classesA = new Class[classes.size()];
    classes.toArray(classesA);
    return classesA;
  } 

  /**
   * Protected constructor for subclasses
   */
  protected InstanceConverter() {
  }

  /**
   * Method to convert one type of object instance to another
   * @param obj
   * @return
   */
  public abstract Object convert(Object obj);

  /**
   * Decode a URL encoded string
   * 
   * @param encoded
   *          the encoded string
   * @return a decoded String
   */
  public static String urlDecode(String encoded) {
    StringBuilder sb = new StringBuilder();

    final int lookingForEncodedCharacter = 0;
    final int lookingForHighOrderByte = 1;
    final int lookingForLowOrderByte = 2;
    int state = lookingForEncodedCharacter;
    char decoded = 0;
    for(char ch:encoded.toCharArray()){
      switch (state) {
      case lookingForEncodedCharacter:
        if('%' == ch){
          state = lookingForHighOrderByte;
        }
        else{
          sb.append(ch);
        }
        break;

      case lookingForHighOrderByte:
        int value = Character.digit(ch, 16);
        value = (value << 4);
        decoded = (char) value;
        state = lookingForLowOrderByte;
        break;

      case lookingForLowOrderByte:
        value = Character.digit(ch, 16);
        decoded |= (char) value;
        sb.append(decoded);
        state = lookingForEncodedCharacter;
        break;
      }
    }

    return sb.toString();
  }

  /**
   * Key for use in hash maps for storing converters
   * @author Thom Hehl
   */
  private static class ConverterKey{
    private Class<?>  fromClass;
    private Class<?>  toClass;

    /**
     * Fully-qualified constructor
     * @param fromClass the class being converted from
     * @param toClass the class being converted to
     */
    public ConverterKey(Class<?> fromClass, Class<?> toClass) {
      super();
      this.fromClass = fromClass;
      this.toClass = toClass;
    }

    /* (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((fromClass == null) ? 0 : fromClass.hashCode());
      result = prime * result + ((toClass == null) ? 0 : toClass.hashCode());
      return result;
    }

    /* (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (getClass() != obj.getClass())
        return false;
      ConverterKey other = (ConverterKey) obj;
      if (fromClass == null) {
        if (other.fromClass != null)
          return false;
      }
      else if (!fromClass.equals(other.fromClass))
        return false;
      if (toClass == null) {
        if (other.toClass != null)
          return false;
      }
      else if (!toClass.equals(other.toClass))
        return false;
      return true;
    }
  }
}
